import time
from typing import Dict
from secrets import token_bytes
from typing import Tuple
from typing import Generator
from typing import TypeVar, Generic, List
from utils.Logging import Logger
from config import setting
from functools import lru_cache
from sys import getsizeof
import matplotlib.pyplot as plt
import math

log = Logger(filename=setting.LOGGING_FILE, level=setting.Logging_level)

times = 0


class CallingCounter:
    def __init__(self, func):
        self.count = 1
        self.func = func

    def __call__(self, *args, **kwargs):
        log.logger.debug(
            'call {} times: {}, get argument:({}, {})'.format(self.func.__name__, self.count, args, kwargs))
        self.count += 1
        start = time.time()
        self.ret = self.func(*args, **kwargs)
        self.spend_time = time.time() - start
        return self.ret


class CompressedGene:
    def __init__(self, gene: str):
        self._compress(gene)

    def _compress(self, gene: str):
        self.bit_string = 1
        for nucleotide in gene.upper():
            self.bit_string <<= 2

            if nucleotide == "A":  # change last two bite to 00
                self.bit_string |= 0b00
            elif nucleotide == "C":
                self.bit_string |= 0b01
            elif nucleotide == "G":
                self.bit_string |= 0b10
            elif nucleotide == "T":
                self.bit_string |= 0b11

            else:
                raise ValueError("Invalid Nucleotide:{}".format(nucleotide))
            log.logger.debug("self.bit_string:{};nucleotide:{}".format(bin(self.bit_string), nucleotide))
            self.compress_rate = getsizeof(self.bit_string) / getsizeof(gene)
            log.logger.debug("compress rate:{}".format(self.compress_rate))

    def decompress(self) -> str:
        gene: str = ""
        for i in range(0, self.bit_string.bit_length() - 1, 2):
            log.logger.debug("self.bit_string:{}".format(bin(self.bit_string)))
            bits: int = self.bit_string >> i & 0b11
            log.logger.debug("bits:{}".format(bin(bits)))
            if bits == 0b00:
                gene += "A"
            elif bits == 0b01:
                gene += "C"
            elif bits == 0b10:
                gene += "G"
            elif bits == 0b11:
                gene += "T"
            else:
                raise ValueError("Invalid bits:{}".format(bits))
            log.logger.debug("gene:{}".format(gene))
        return gene[::-1]

    def __str__(self):
        return self.decompress()

    def __repr__(self):
        return "Gene({})".format(self.decompress())


@CallingCounter
def fibonacci_sequence(index):
    if index < 2:
        return index
    return fibonacci_sequence(index - 1) + fibonacci_sequence(index - 2)


def fib2(index):
    return fibonacci_sequence(index)


memo: Dict[int, int] = {0: 0, 1: 1}  # base case


@CallingCounter
def fib3(index) -> int:
    # Fibonacci sequence with memoization
    if index not in memo:
        memo[index] = fib3(index - 1) + fib3(index - 2)
    return memo[index]


@CallingCounter
@lru_cache(maxsize=None)
def fib4(index):
    if index < 2:
        return index
    return fib4(index - 1) + fib4(index - 2)


def fib5(index):
    if index == 0: return 0
    latest: int = 0
    nextt: int = 1
    for _ in range(1, index):
        latest, nextt = nextt, latest + nextt
    return nextt


def fib6(index) -> Generator[int, None, None]:
    yield 0
    if index > 0: yield 1
    latest: int = 0
    next: int = 1
    for _ in range(1, index):
        latest, next = next, next + latest
        yield next


def random_key(length: int):
    # generate length random bytes
    tb: bytes = token_bytes(length)
    print(tb)
    return int.from_bytes(tb, 'big')


def encrypt(original: str) -> Tuple[int, int]:
    original_bytes = original.encode()
    dummy: int = random_key(len(original_bytes))
    original_key: int = int.from_bytes(original_bytes, 'big')
    encrypted: int = original_key ^ dummy  # XOR
    return dummy, encrypted


def decrypt(key1: int, key2: int) -> str:
    decrypted: int = key1 ^ key2
    temp: bytes = decrypted.to_bytes((decrypted.bit_length() + 7) // 8, 'big')
    return temp.decode()


def calculate_pi(n_terms: int) -> float:
    numerator: float = 4.0
    denominator: float = 1.0
    operation: float = 1.0
    pi: float = 0.0
    for i in range(n_terms):
        pi += operation * numerator / denominator
        denominator += 2
        operation *= -1
    return pi


T = TypeVar('T')


class Stack(Generic[T]):
    def __init__(self) -> None:
        self._container: List[T] = []

    def push(self, item: T) -> None:
        self._container.append(item)

    def pop(self) -> T:
        return self._container.pop()

    def __repr__(self):
        return repr(self._container)

    def get_top(self):
        return self._container[-1]


def hanoi(begin: Stack[int], end: Stack[int], temp: Stack[int], n_discs: int) -> None:
    if n_discs == 1:
        end.push(begin.pop())
    else:
        hanoi(begin, temp, end, n_discs - 1)
        hanoi(begin, end, temp, 1)
        hanoi(temp, end, begin, n_discs - 1)


if __name__ == '__main__':
    num_discs: int = 3
    tower_a: Stack[int] = Stack()
    tower_b: Stack[int] = Stack()
    tower_c: Stack[int] = Stack()
    for i in range(1, num_discs + 1):
        tower_a.push(i)

    hanoi(tower_a, tower_c, tower_b, num_discs)
    print(tower_a, tower_b, tower_c)
